angular.module('textAngular.DOM', ['textAngular.factories'])
.factory('taExecCommand', ['taSelection', 'taBrowserTag', '$document', function(taSelection, taBrowserTag, $document){
	var listToDefault = function(listElement, defaultWrap){
		var $target, i;
		// if all selected then we should remove the list
		// grab all li elements and convert to taDefaultWrap tags
		var children = listElement.find('li');
		for(i = children.length - 1; i >= 0; i--){
			$target = angular.element('<' + defaultWrap + '>' + children[i].innerHTML + '</' + defaultWrap + '>');
			listElement.after($target);
		}
		listElement.remove();
		taSelection.setSelectionToElementEnd($target[0]);
	};
	var listElementToSelfTag = function(list, listElement, selfTag, bDefault, defaultWrap){
		var $target, i;
		// if all selected then we should remove the list
		// grab all li elements
		var priorElement;
		var nextElement;
		var children = list.find('li');
		var foundIndex;
		for (i = 0; i<children.length; i++) {
			if (children[i].outerHTML === listElement[0].outerHTML) {
				// found it...
				foundIndex = i;
				if (i>0) {
					priorElement = children[i-1];
				}
				if (i+1<children.length) {
					nextElement = children[i+1];
				}
				break;
			}
		}
		//console.log('listElementToSelfTag', list, listElement, selfTag, bDefault, priorElement, nextElement);
		// un-list the listElement
		var html = '';
		if (bDefault) {
			html += '<' + defaultWrap + '>' + listElement[0].innerHTML + '</' + defaultWrap + '>';
		} else {
			html += '<' + taBrowserTag(selfTag) + '>';
			html += '<li>' + listElement[0].innerHTML + '</li>';
			html += '</' + taBrowserTag(selfTag) + '>';
		}
		$target = angular.element(html);
		//console.log('$target', $target[0]);
		if (!priorElement) {
			// this is the first the list, so we just remove it...
			listElement.remove();
			list.after(angular.element(list[0].outerHTML));
			list.after($target);
			list.remove();
			taSelection.setSelectionToElementEnd($target[0]);
			return;
		} else if (!nextElement) {
			// this is the last in the list, so we just remove it..
			listElement.remove();
			list.after($target);
			taSelection.setSelectionToElementEnd($target[0]);
		} else {
			var p = list.parent();
			// okay it was some where in the middle... so we need to break apart the list...
			var html1 = '';
			var listTag = list[0].nodeName.toLowerCase();
			html1 += '<' + listTag + '>';
			for(i = 0; i < foundIndex; i++){
				html1 += '<li>' + children[i].innerHTML + '</li>';
			}
			html1 += '</' + listTag + '>';
			var html2 = '';
			html2 += '<' + listTag + '>';
			for(i = foundIndex+1; i < children.length; i++){
				html2 += '<li>' + children[i].innerHTML + '</li>';
			}
			html2 += '</' + listTag + '>';
			//console.log(html1, $target[0], html2);
			list.after(angular.element(html2));
			list.after($target);
			list.after(angular.element(html1));
			list.remove();
			//console.log('parent ******XXX*****', p[0]);
			taSelection.setSelectionToElementEnd($target[0]);
		}
	};
	var listElementsToSelfTag = function(list, listElements, selfTag, bDefault, defaultWrap){
		var $target, i, j, p;
		// grab all li elements
		var priorElement;
		var afterElement;
		//console.log('list:', list, 'listElements:', listElements, 'selfTag:', selfTag, 'bDefault:', bDefault);
		var children = list.find('li');
		var foundIndexes = [];
		for (i = 0; i<children.length; i++) {
			for (j = 0; j<listElements.length; j++) {
				if (children[i].isEqualNode(listElements[j])) {
					// found it...
					foundIndexes[j] = i;
				}
			}
		}
		if (foundIndexes[0] > 0) {
			priorElement = children[foundIndexes[0] - 1];
		}
		if (foundIndexes[listElements.length-1] + 1 < children.length) {
			afterElement = children[foundIndexes[listElements.length-1] + 1];
		}
		//console.log('listElementsToSelfTag', list, listElements, selfTag, bDefault, !priorElement, !afterElement, foundIndexes[listElements.length-1], children.length);
		// un-list the listElements
		var html = '';
		if (bDefault) {
			for (j = 0; j < listElements.length; j++) {
				html += '<' + defaultWrap + '>' + listElements[j].innerHTML + '</' + defaultWrap + '>';
				listElements[j].remove();
			}
		} else {
			html += '<' + taBrowserTag(selfTag) + '>';
			for (j = 0; j < listElements.length; j++) {
				html += listElements[j].outerHTML;
				listElements[j].remove();
			}
			html += '</' + taBrowserTag(selfTag) + '>';
		}
		$target = angular.element(html);
		if (!priorElement) {
			// this is the first the list, so we just remove it...
			list.after(angular.element(list[0].outerHTML));
			list.after($target);
			list.remove();
			taSelection.setSelectionToElementEnd($target[0]);
			return;
		} else if (!afterElement) {
			// this is the last in the list, so we just remove it..
			list.after($target);
			taSelection.setSelectionToElementEnd($target[0]);
			return;
		} else {
			// okay it was some where in the middle... so we need to break apart the list...
			var html1 = '';
			var listTag = list[0].nodeName.toLowerCase();
			html1 += '<' + listTag + '>';
			for(i = 0; i < foundIndexes[0]; i++){
				html1 += '<li>' + children[i].innerHTML + '</li>';
			}
			html1 += '</' + listTag + '>';
			var html2 = '';
			html2 += '<' + listTag + '>';
			for(i = foundIndexes[listElements.length-1]+1; i < children.length; i++){
				html2 += '<li>' + children[i].innerHTML + '</li>';
			}
			html2 += '</' + listTag + '>';
			list.after(angular.element(html2));
			list.after($target);
			list.after(angular.element(html1));
			list.remove();
			//console.log('parent ******YYY*****', list.parent()[0]);
			taSelection.setSelectionToElementEnd($target[0]);
		}
	};
	var selectLi = function(liElement){
		if(/(<br(|\/)>)$/i.test(liElement.innerHTML.trim())) taSelection.setSelectionBeforeElement(angular.element(liElement).find("br")[0]);
		else taSelection.setSelectionToElementEnd(liElement);
	};
	var listToList = function(listElement, newListTag){
		var $target = angular.element('<' + newListTag + '>' + listElement[0].innerHTML + '</' + newListTag + '>');
		listElement.after($target);
		listElement.remove();
		selectLi($target.find('li')[0]);
	};
	var childElementsToList = function(elements, listElement, newListTag){
		var html = '';
		for(var i = 0; i < elements.length; i++){
			html += '<' + taBrowserTag('li') + '>' + elements[i].innerHTML + '</' + taBrowserTag('li') + '>';
		}
		var $target = angular.element('<' + newListTag + '>' + html + '</' + newListTag + '>');
		listElement.after($target);
		listElement.remove();
		selectLi($target.find('li')[0]);
	};
	var turnBlockIntoBlocks = function(element, options) {
		for(var i = 0; i<element.childNodes.length; i++) {
			var _n = element.childNodes[i];
			/* istanbul ignore next - more complex testing*/
			if (_n.tagName && _n.tagName.match(BLOCKELEMENTS)) {
				turnBlockIntoBlocks(_n, options);
			}
		}
		/* istanbul ignore next - very rare condition that we do not test*/
		if (element.parentNode === null) {
			// nothing left to do..
			return element;
		}
		/* istanbul ignore next - not sure have to test this */
		if (options === '<br>'){
			return element;
		}
		else {
			var $target = angular.element(options);
			$target[0].innerHTML = element.innerHTML;
			element.parentNode.insertBefore($target[0], element);
			element.parentNode.removeChild(element);
			return $target;
		}
	};
	return function(taDefaultWrap, topNode){
		// NOTE: here we are dealing with the html directly from the browser and not the html the user sees.
		// IF you want to modify the html the user sees, do it when the user does a switchView
		taDefaultWrap = taBrowserTag(taDefaultWrap);
		return function(command, showUI, options, defaultTagAttributes){
			var i, $target, html, _nodes, next, optionsTagName, selectedElement, ourSelection;
			var defaultWrapper = angular.element('<' + taDefaultWrap + '>');
			try{
				if (taSelection.getSelection) {
					ourSelection = taSelection.getSelection();
				}
				selectedElement = taSelection.getSelectionElement();
				// special checks and fixes when we are selecting the whole container
				var __h, _innerNode;
				/* istanbul ignore next */
                if (selectedElement.tagName !== undefined) {
                    if (selectedElement.tagName.toLowerCase() === 'div' &&
                        /taTextElement.+/.test(selectedElement.id) &&
                        ourSelection && ourSelection.start &&
                        ourSelection.start.offset === 1 &&
                        ourSelection.end.offset === 1) {
                        // opps we are actually selecting the whole container!
                        //console.log('selecting whole container!');
                        __h = selectedElement.innerHTML;
                        if (/<br>/i.test(__h)) {
                            // Firefox adds <br>'s and so we remove the <br>
                            __h = __h.replace(/<br>/i, '&#8203;');  // no space-space
                        }
                        if (/<br\/>/i.test(__h)) {
                            // Firefox adds <br/>'s and so we remove the <br/>
                            __h = __h.replace(/<br\/>/i, '&#8203;');  // no space-space
                        }
                        // remove stacked up <span>'s
                        if (/<span>(<span>)+/i.test(__h)) {
                            __h = __.replace(/<span>(<span>)+/i, '<span>');
                        }
                        // remove stacked up </span>'s
                        if (/<\/span>(<\/span>)+/i.test(__h)) {
                            __h = __.replace(/<\/span>(<\/span>)+/i, '<\/span>');
                        }
                        if (/<span><\/span>/i.test(__h)) {
                            // if we end up with a <span></span> here we remove it...
                            __h = __h.replace(/<span><\/span>/i, '');
                        }
                        //console.log('inner whole container', selectedElement.childNodes);
                        _innerNode = '<div>' + __h + '</div>';
                        selectedElement.innerHTML = _innerNode;
                        taSelection.setSelectionToElementEnd(selectedElement.childNodes[0]);
                        selectedElement = taSelection.getSelectionElement();
                    } else if (selectedElement.tagName.toLowerCase() === 'span' &&
                        ourSelection && ourSelection.start &&
                        ourSelection.start.offset === 1 &&
                        ourSelection.end.offset === 1) {
                        // just a span -- this is a problem...
                        //console.log('selecting span!');
                        __h = selectedElement.innerHTML;
                        if (/<br>/i.test(__h)) {
                            // Firefox adds <br>'s and so we remove the <br>
                            __h = __h.replace(/<br>/i, '&#8203;');  // no space-space
                        }
                        if (/<br\/>/i.test(__h)) {
                            // Firefox adds <br/>'s and so we remove the <br/>
                            __h = __h.replace(/<br\/>/i, '&#8203;');  // no space-space
                        }
                        // remove stacked up <span>'s
                        if (/<span>(<span>)+/i.test(__h)) {
                            __h = __.replace(/<span>(<span>)+/i, '<span>');
                        }
                        // remove stacked up </span>'s
                        if (/<\/span>(<\/span>)+/i.test(__h)) {
                            __h = __.replace(/<\/span>(<\/span>)+/i, '<\/span>');
                        }
                        if (/<span><\/span>/i.test(__h)) {
                            // if we end up with a <span></span> here we remove it...
                            __h = __h.replace(/<span><\/span>/i, '');
                        }
                        //console.log('inner span', selectedElement.childNodes);
                        // we wrap this in a <div> because otherwise the browser get confused when we attempt to select the whole node
                        // and the focus is not set correctly no matter what we do
                        _innerNode = '<div>' + __h + '</div>';
                        selectedElement.innerHTML = _innerNode;
                        taSelection.setSelectionToElementEnd(selectedElement.childNodes[0]);
                        selectedElement = taSelection.getSelectionElement();
                        //console.log(selectedElement.innerHTML);
                    } else if (selectedElement.tagName.toLowerCase() === 'p' &&
                        ourSelection && ourSelection.start &&
                        ourSelection.start.offset === 1 &&
                        ourSelection.end.offset === 1) {
                        //console.log('p special');
                        // we need to remove the </br> that firefox adds!
                        __h = selectedElement.innerHTML;
                        if (/<br>/i.test(__h)) {
                            // Firefox adds <br>'s and so we remove the <br>
                            __h = __h.replace(/<br>/i, '&#8203;');  // no space-space
							selectedElement.innerHTML = __h;
						}
                    } else if (selectedElement.tagName.toLowerCase() === 'li' &&
                        ourSelection && ourSelection.start &&
                        ourSelection.start.offset === ourSelection.end.offset) {
                        // we need to remove the </br> that firefox adds!
                        __h = selectedElement.innerHTML;
                        if (/<br>/i.test(__h)) {
                            // Firefox adds <br>'s and so we remove the <br>
                            __h = __h.replace(/<br>/i, '');  // nothing
							selectedElement.innerHTML = __h;
                        }
                    }
                }
            }catch(e){}
			//console.log('************** selectedElement:', selectedElement);
			/* istanbul ignore if: */
			if (!selectedElement){return;}
			var $selected = angular.element(selectedElement);
			var tagName = (selectedElement && selectedElement.tagName && selectedElement.tagName.toLowerCase()) ||
				/* istanbul ignore next: */ "";
			if(command.toLowerCase() === 'insertorderedlist' || command.toLowerCase() === 'insertunorderedlist'){
				var selfTag = taBrowserTag((command.toLowerCase() === 'insertorderedlist')? 'ol' : 'ul');
				var selectedElements = taSelection.getOnlySelectedElements();
				//console.log('PPPPPPPPPPPPP', tagName, selfTag, selectedElements, tagName.match(BLOCKELEMENTS), $selected.hasClass('ta-bind'), $selected.parent()[0].tagName);
				if (selectedElements.length>1 && (tagName === 'ol' ||  tagName === 'ul' )) {
					return listElementsToSelfTag($selected, selectedElements, selfTag, selfTag===tagName, taDefaultWrap);
				}
				if(tagName === selfTag){
					// if all selected then we should remove the list
					// grab all li elements and convert to taDefaultWrap tags
					//console.log('tagName===selfTag');
					if ($selected[0].childNodes.length !== selectedElements.length && selectedElements.length===1) {
						$selected = angular.element(selectedElements[0]);
						return listElementToSelfTag($selected.parent(), $selected, selfTag, true, taDefaultWrap);
					} else {
						return listToDefault($selected, taDefaultWrap);
					}
				}else if(tagName === 'li' &&
					$selected.parent()[0].tagName.toLowerCase() === selfTag &&
					$selected.parent().children().length === 1){
					// catch for the previous statement if only one li exists
					return listToDefault($selected.parent(), taDefaultWrap);
				}else if(tagName === 'li' &&
					$selected.parent()[0].tagName.toLowerCase() !== selfTag &&
					$selected.parent().children().length === 1){
					// catch for the previous statement if only one li exists
					return listToList($selected.parent(), selfTag);
				}else if(tagName.match(BLOCKELEMENTS) && !$selected.hasClass('ta-bind')){
					// if it's one of those block elements we have to change the contents
					// if it's a ol/ul we are changing from one to the other
					if (selectedElements.length) {
						if ($selected[0].childNodes.length !== selectedElements.length && selectedElements.length===1) {
							//console.log('&&&&&&&&&&&&&&& --------- &&&&&&&&&&&&&&&&', selectedElements[0], $selected[0].childNodes);
							$selected = angular.element(selectedElements[0]);
							return listElementToSelfTag($selected.parent(), $selected, selfTag, selfTag===tagName, taDefaultWrap);
						}
					}
					if(tagName === 'ol' || tagName === 'ul'){
						// now if this is a set of selected elements... behave diferently
						return listToList($selected, selfTag);
					}else{
						var childBlockElements = false;
						angular.forEach($selected.children(), function(elem){
							if(elem.tagName.match(BLOCKELEMENTS)) {
								childBlockElements = true;
							}
						});
						if(childBlockElements){
							return childElementsToList($selected.children(), $selected, selfTag);
						}else{
							return childElementsToList([angular.element('<div>' + selectedElement.innerHTML + '</div>')[0]], $selected, selfTag);
						}
					}
				}else if(tagName.match(BLOCKELEMENTS)){
					// if we get here then the contents of the ta-bind are selected
					_nodes = taSelection.getOnlySelectedElements();
					//console.log('_nodes', _nodes, tagName);
					if(_nodes.length === 0){
						// here is if there is only text in ta-bind ie <div ta-bind>test content</div>
						$target = angular.element('<' + selfTag + '><li>' + selectedElement.innerHTML + '</li></' + selfTag + '>');
						$selected.html('');
						$selected.append($target);
					}else if(_nodes.length === 1 && (_nodes[0].tagName.toLowerCase() === 'ol' || _nodes[0].tagName.toLowerCase() === 'ul')){
						if(_nodes[0].tagName.toLowerCase() === selfTag){
							// remove
							return listToDefault(angular.element(_nodes[0]), taDefaultWrap);
						}else{
							return listToList(angular.element(_nodes[0]), selfTag);
						}
					}else{
						html = '';
						var $nodes = [];
						for(i = 0; i < _nodes.length; i++){
							/* istanbul ignore else: catch for real-world can't make it occur in testing */
							if(_nodes[i].nodeType !== 3){
								var $n = angular.element(_nodes[i]);
								/* istanbul ignore if: browser check only, phantomjs doesn't return children nodes but chrome at least does */
								if(_nodes[i].tagName.toLowerCase() === 'li') continue;
								else if(_nodes[i].tagName.toLowerCase() === 'ol' || _nodes[i].tagName.toLowerCase() === 'ul'){
									html += $n[0].innerHTML; // if it's a list, add all it's children
								}else if(_nodes[i].tagName.toLowerCase() === 'span' && (_nodes[i].childNodes[0].tagName.toLowerCase() === 'ol' || _nodes[i].childNodes[0].tagName.toLowerCase() === 'ul')){
									html += $n[0].childNodes[0].innerHTML; // if it's a list, add all it's children
								}else{
									html += '<' + taBrowserTag('li') + '>' + $n[0].innerHTML + '</' + taBrowserTag('li') + '>';
								}
								$nodes.unshift($n);
							}
						}
						//console.log('$nodes', $nodes);
						$target = angular.element('<' + selfTag + '>' + html + '</' + selfTag + '>');
						$nodes.pop().replaceWith($target);
						angular.forEach($nodes, function($node){ $node.remove(); });
					}
					taSelection.setSelectionToElementEnd($target[0]);
					return;
				}
			}else if(command.toLowerCase() === 'formatblock'){
				optionsTagName = options.toLowerCase().replace(/[<>]/ig, '');
				if(optionsTagName.trim() === 'default') {
					optionsTagName = taDefaultWrap;
					options = '<' + taDefaultWrap + '>';
				}
				if(tagName === 'li') {
					$target = $selected.parent();
				}
				else {
					$target = $selected;
				}
				// find the first blockElement
				while(!$target[0].tagName || !$target[0].tagName.match(BLOCKELEMENTS) && !$target.parent().attr('contenteditable')){
					$target = $target.parent();
					/* istanbul ignore next */
					tagName = ($target[0].tagName || '').toLowerCase();
				}
				if(tagName === optionsTagName){
					// $target is wrap element
					_nodes = $target.children();
					var hasBlock = false;
					for(i = 0; i < _nodes.length; i++){
						hasBlock = hasBlock || _nodes[i].tagName.match(BLOCKELEMENTS);
					}
					if(hasBlock){
						$target.after(_nodes);
						next = $target.next();
						$target.remove();
						$target = next;
					}else{
						defaultWrapper.append($target[0].childNodes);
						$target.after(defaultWrapper);
						$target.remove();
						$target = defaultWrapper;
					}
				}else if($target.parent()[0].tagName.toLowerCase() === optionsTagName &&
					!$target.parent().hasClass('ta-bind')){
					//unwrap logic for parent
					var blockElement = $target.parent();
					var contents = blockElement.contents();
					for(i = 0; i < contents.length; i ++){
						/* istanbul ignore next: can't test - some wierd thing with how phantomjs works */
						if(blockElement.parent().hasClass('ta-bind') && contents[i].nodeType === 3){
							defaultWrapper = angular.element('<' + taDefaultWrap + '>');
							defaultWrapper[0].innerHTML = contents[i].outerHTML;
							contents[i] = defaultWrapper[0];
						}
						blockElement.parent()[0].insertBefore(contents[i], blockElement[0]);
					}
					blockElement.remove();
				}else if(tagName.match(LISTELEMENTS)){
					// wrapping a list element
					$target.wrap(options);
				}else{
					// default wrap behaviour
					_nodes = taSelection.getOnlySelectedElements();
					if(_nodes.length === 0) {
						// no nodes at all....
						_nodes = [$target[0]];
					}
					// find the parent block element if any of the nodes are inline or text
					for(i = 0; i < _nodes.length; i++){
						if(_nodes[i].nodeType === 3 || !_nodes[i].tagName.match(BLOCKELEMENTS)){
							while(_nodes[i].nodeType === 3 || !_nodes[i].tagName || !_nodes[i].tagName.match(BLOCKELEMENTS)){
								_nodes[i] = _nodes[i].parentNode;
							}
						}
					}
					// remove any duplicates from the array of _nodes!
					_nodes = _nodes.filter(function(value, index, self) {
						return self.indexOf(value) === index;
					});
					// remove all whole taTextElement if it is here... unless it is the only element!
					if (_nodes.length>1) {
						_nodes = _nodes.filter(function (value, index, self) {
							return !(value.nodeName.toLowerCase() === 'div' && /^taTextElement/.test(value.id));
						});
					}
					if(angular.element(_nodes[0]).hasClass('ta-bind')){
						$target = angular.element(options);
						$target[0].innerHTML = _nodes[0].innerHTML;
						_nodes[0].innerHTML = $target[0].outerHTML;
					}else if(optionsTagName === 'blockquote'){
						// blockquotes wrap other block elements
						html = '';
						for(i = 0; i < _nodes.length; i++){
							html += _nodes[i].outerHTML;
						}
						$target = angular.element(options);
						$target[0].innerHTML = html;
						_nodes[0].parentNode.insertBefore($target[0],_nodes[0]);
						for(i = _nodes.length - 1; i >= 0; i--){
							/* istanbul ignore else:  */
							if (_nodes[i].parentNode) _nodes[i].parentNode.removeChild(_nodes[i]);
						}
					} else /* istanbul ignore next: not tested since identical to blockquote */
					if (optionsTagName === 'pre' && taSelection.getStateShiftKey()) {
						//console.log('shift pre', _nodes);
						// pre wrap other block elements
						html = '';
						for (i = 0; i < _nodes.length; i++) {
							html += _nodes[i].outerHTML;
						}
						$target = angular.element(options);
						$target[0].innerHTML = html;
						_nodes[0].parentNode.insertBefore($target[0], _nodes[0]);
						for (i = _nodes.length - 1; i >= 0; i--) {
							/* istanbul ignore else:  */
							if (_nodes[i].parentNode) _nodes[i].parentNode.removeChild(_nodes[i]);
						}
					}
					else {
						//console.log(optionsTagName, _nodes);
						// regular block elements replace other block elements
						for (i = 0; i < _nodes.length; i++) {
							var newBlock = turnBlockIntoBlocks(_nodes[i], options);
							if (_nodes[i] === $target[0]) {
								$target = angular.element(newBlock);
							}
						}
					}
				}
				taSelection.setSelectionToElementEnd($target[0]);
				// looses focus when we have the whole container selected and no text!
				// refocus on the shown display element, this fixes a bug when using firefox
				$target[0].focus();
				return;
			}else if(command.toLowerCase() === 'createlink'){
				/* istanbul ignore next: firefox specific fix */
				if (tagName === 'a') {
					// already a link!!! we are just replacing it...
					taSelection.getSelectionElement().href = options;
					return;
				}
				var tagBegin = '<a href="' + options + '" target="' +
						(defaultTagAttributes.a.target ? defaultTagAttributes.a.target : '') +
						'">',
					tagEnd = '</a>',
					_selection = taSelection.getSelection();
				if(_selection.collapsed){
					//console.log('collapsed');
					// insert text at selection, then select then just let normal exec-command run
					taSelection.insertHtml(tagBegin + options + tagEnd, topNode);
				}else if(rangy.getSelection().getRangeAt(0).canSurroundContents()){
					var node = angular.element(tagBegin + tagEnd)[0];
					rangy.getSelection().getRangeAt(0).surroundContents(node);
				}
				return;
			}else if(command.toLowerCase() === 'inserthtml'){
				//console.log('inserthtml');
				taSelection.insertHtml(options, topNode);
				return;
			}
			try{
				$document[0].execCommand(command, showUI, options);
			}catch(e){}
		};
	};
}]).service('taSelection', ['$document', 'taDOM', '$log',
/* istanbul ignore next: all browser specifics and PhantomJS dosen't seem to support half of it */
function($document, taDOM, $log){
	// need to dereference the document else the calls don't work correctly
	var _document = $document[0];
	var bShiftState;
	var brException = function (element, offset) {
		/* check if selection is a BR element at the beginning of a container. If so, get
		* the parentNode instead.
		* offset should be zero in this case. Otherwise, return the original
		* element.
		*/
		if (element.tagName && element.tagName.match(/^br$/i) && offset === 0 && !element.previousSibling) {
            return {
                element: element.parentNode,
                offset: 0
            };
		} else {
			return {
				element: element,
				offset: offset
			};
		}
	};
	var api = {
		getSelection: function(){
			var range;
			try {
				// catch any errors from rangy and ignore the issue
				range = rangy.getSelection().getRangeAt(0);
			} catch(e) {
				//console.info(e);
				return undefined;
			}
			var container = range.commonAncestorContainer;
            var selection = {
				start: brException(range.startContainer, range.startOffset),
				end: brException(range.endContainer, range.endOffset),
				collapsed: range.collapsed
			};
            // This has problems under Firefox.
            // On Firefox with
            // <p>Try me !</p>
            // <ul>
            // <li>line 1</li>
            // <li>line 2</li>
            // </ul>
            // <p>line 3</p>
            // <ul>
            // <li>line 4</li>
            // <li>line 5</li>
            // </ul>
            // <p>Hello textAngular</p>
            // WITH the cursor after the 3 on line 3, it gets the commonAncestorContainer as:
            // <TextNode textContent='line 3'>
            // AND Chrome gets the commonAncestorContainer as:
            // <p>line 3</p>
            //
			// Check if the container is a text node and return its parent if so
            // unless this is the whole taTextElement.  If so we return the textNode
			if (container.nodeType === 3) {
                if (container.parentNode.nodeName.toLowerCase() === 'div' &&
                    /^taTextElement/.test(container.parentNode.id)) {
                    // textNode where the parent is the whole <div>!!!
					//console.log('textNode ***************** container:', container);
                } else {
                    container = container.parentNode;
                }
            }
			if (container.nodeName.toLowerCase() === 'div' &&
				/^taTextElement/.test(container.id)) {
				//console.log('*********taTextElement************');
				//console.log('commonAncestorContainer:', container);
				selection.start.element = container.childNodes[selection.start.offset];
				selection.end.element = container.childNodes[selection.end.offset];
				selection.container = container;
			} else {
				if (container.parentNode === selection.start.element ||
					container.parentNode === selection.end.element) {
					selection.container = container.parentNode;
				} else {
					selection.container = container;
				}
			}
			//console.log('***selection container:', selection.container.nodeName, selection.start.offset, selection.container);
			return selection;
		},
		// if we use the LEFT_ARROW and we are at the special place <span>&#65279;</span> we move the cursor over by one...
		// Chrome and Firefox behave differently so so fix this for Firefox here.  No adjustment needed for Chrome.
		updateLeftArrowKey: function(element) {
			var range = rangy.getSelection().getRangeAt(0);
			if (range && range.collapsed) {
				var _nodes = api.getFlattenedDom(range);
				if (!_nodes.findIndex) return;
				var _node = range.startContainer;
				var indexStartContainer = _nodes.findIndex(function(element, index){
					if (element.node===_node) return true;
					var _indexp = element.parents.indexOf(_node);
					return (_indexp !== -1);
				});
				var m;
				var nextNodeToRight;
				//console.log('indexStartContainer', indexStartContainer, _nodes.length, 'startContainer:', _node, _node === _nodes[indexStartContainer].node);
				_nodes.forEach(function (n, i) {
					//console.log(i, n.node);
					n.parents.forEach(function (nn, j){
						//console.log(i, j, nn);
					});
				});
				if (indexStartContainer+1 < _nodes.length) {
					// we need the node just after this startContainer
					// so we can check and see it this is a special place
					nextNodeToRight = _nodes[indexStartContainer+1].node;
					//console.log(nextNodeToRight, range.startContainer);
				}
				//console.log('updateLeftArrowKey', range.startOffset, range.startContainer.textContent);
				// this first section handles the case for Chrome browser
				// if the first character of the nextNode is a \ufeff we know that we are just before the special span...
				// and so we most left by one character
				if (nextNodeToRight && nextNodeToRight.textContent) {
					m = /^\ufeff([^\ufeff]*)$/.exec(nextNodeToRight.textContent);
					if (m) {
						// we are before the special node with begins with a \ufeff character
						//console.log('LEFT ...found it...', 'startOffset:', range.startOffset, m[0].length, m[1].length);
						// no need to change anything in this case
						return;
					}
				}
				var nextNodeToLeft;
				if (indexStartContainer > 0) {
					// we need the node just after this startContainer
					// so we can check and see it this is a special place
					nextNodeToLeft = _nodes[indexStartContainer-1].node;
					//console.log(nextNodeToLeft, nextNodeToLeft);
				}
				if (range.startOffset === 0 && nextNodeToLeft) {
					//console.log(nextNodeToLeft, range.startOffset, nextNodeToLeft.textContent);
					m = /^\ufeff([^\ufeff]*)$/.exec(nextNodeToLeft.textContent);
					if (m) {
						//console.log('LEFT &&&&&&&&&&&&&&&&&&&...found it...&&&&&&&&&&&', nextNodeToLeft, m[0].length, m[1].length);
						// move over to the left my one -- Firefox triggers this case
						api.setSelectionToElementEnd(nextNodeToLeft);
						return;
					}
				}
			}
			return;
		},
		// if we use the RIGHT_ARROW and we are at the special place <span>&#65279;</span> we move the cursor over by one...
		updateRightArrowKey: function(element) {
			// we do not need to make any adjustments here, so we ignore all this code
			if (false) {
				var range = rangy.getSelection().getRangeAt(0);
				if (range && range.collapsed) {
					var _nodes = api.getFlattenedDom(range);
					if (!_nodes.findIndex) return;
					var _node = range.startContainer;
					var indexStartContainer = _nodes.findIndex(function (element, index) {
						if (element.node === _node) return true;
						var _indexp = element.parents.indexOf(_node);
						return (_indexp !== -1);
					});
					var _sel;
					var i;
					var m;

					// if the last character is a \ufeff we know that we are just before the special span...
					// and so we most right by one character
					var indexFound = _nodes.findIndex(function (n, index) {
						if (n.textContent) {
							var m = /^\ufeff([^\ufeff]*)$/.exec(n.textContent);
							if (m) {
								return true;
							} else {
								return false;
							}
						} else {
							return false;
						}
					});
					if (indexFound === -1) {
						return;
					}
					//console.log(indexFound, range.startContainer, range.startOffset);
					_node = _nodes[indexStartContainer];
					//console.log('indexStartContainer', indexStartContainer);
					if (_node && _node.textContent) {
						m = /^\ufeff([^\ufeff]*)$/.exec(_node.textContent);
						if (m && range.startOffset - 1 === m[1].length) {
							//console.log('RIGHT found it...&&&&&&&&&&&', range.startOffset);
							// no need to make any adjustment
							return;
						}
					}
					//console.log(range.startOffset);
					if (_nodes && range.startOffset === 0) {
						indexStartContainer = _nodes.indexOf(range.startContainer);
						if (indexStartContainer !== -1 && indexStartContainer > 0) {
							_node = _nodes[indexStartContainer - 1];
							if (_node.textContent) {
								m = /\ufeff([^\ufeff]*)$/.exec(_node.textContent);
								if (m && true || range.startOffset === m[1].length + 1) {
									//console.log('RIGHT &&&&&&&&&&&&&&&&&&&...found it...&&&&&&&&&&&', range.startOffset, m[1].length);
									// no need to make any adjustment
									return;
								}
							}
						}
					}
				}
			}
		},
		getFlattenedDom: function(range) {
			var parent = range.commonAncestorContainer.parentNode;
			if (!parent) {
				return range.commonAncestorContainer.childNodes;
			}
			var nodes = Array.prototype.slice.call(parent.childNodes); // converts NodeList to Array
			var indexStartContainer = nodes.indexOf(range.startContainer);
			// make sure that we have a big enough set of nodes
			if (indexStartContainer+1 < nodes.length && indexStartContainer > 0) {
				// we are good
				// we can go down one node or up one node
			} else {
				if (parent.parentNode) {
					parent = parent.parentNode;
				}
			}
			// now walk the parent
			nodes = [];
			function addNodes(_set) {
				if (_set.node.childNodes.length) {
					var childNodes = Array.prototype.slice.call(_set.node.childNodes); // converts NodeList to Array
					childNodes.forEach(function(n) {
						var _t = _set.parents.slice();
						if (_t.slice(-1)[0]!==_set.node) {
							_t.push(_set.node);
						}
						addNodes({parents: _t, node: n});
					});
				} else {
					nodes.push({parents: _set.parents, node: _set.node});
				}
			}
			addNodes({parents: [parent], node: parent});
			return nodes;
		},
		getOnlySelectedElements: function(){
			var range = rangy.getSelection().getRangeAt(0);
			var container = range.commonAncestorContainer;
			// Node.TEXT_NODE === 3
			// Node.ELEMENT_NODE === 1
			// Node.COMMENT_NODE === 8
			// Check if the container is a text node and return its parent if so
			container = container.nodeType === 3 ? container.parentNode : container;
			// get the nodes in the range that are ELEMENT_NODE and are children of the container
			// in this range...
			return range.getNodes([1], function(node){
				return node.parentNode === container;
			});
		},
		// this includes the container element if all children are selected
		getAllSelectedElements: function(){
			var range = rangy.getSelection().getRangeAt(0);
			var container = range.commonAncestorContainer;
			// Node.TEXT_NODE === 3
			// Node.ELEMENT_NODE === 1
			// Node.COMMENT_NODE === 8
			// Check if the container is a text node and return its parent if so
			container = container.nodeType === 3 ? container.parentNode : container;
			// get the nodes in the range that are ELEMENT_NODE and are children of the container
			// in this range...
			var selectedNodes = range.getNodes([1], function(node){
				return node.parentNode === container;
			});
			var innerHtml = container.innerHTML;
			// remove the junk that rangy has put down
			innerHtml = innerHtml.replace(/<span id=.selectionBoundary[^>]+>\ufeff?<\/span>/ig, '');
			//console.log(innerHtml);
			//console.log(range.toHtml());
			//console.log(innerHtml === range.toHtml());
			if (innerHtml === range.toHtml() &&
				// not the whole taTextElement
				(!(container.nodeName.toLowerCase() === 'div' &&  /^taTextElement/.test(container.id)))
			) {
				var arr = [];
				for(var i = selectedNodes.length; i--; arr.unshift(selectedNodes[i]));
				selectedNodes = arr;
				selectedNodes.push(container);
				//$log.debug(selectedNodes);
			}
			return selectedNodes;
		},
		// Some basic selection functions
		getSelectionElement: function () {
			var s = api.getSelection();
			if (s) {
				return api.getSelection().container;
			} else {
				return undefined;
			}
		},
		setSelection: function(elStart, elEnd, start, end){
			var range = rangy.createRange();

			range.setStart(elStart, start);
			range.setEnd(elEnd, end);

			rangy.getSelection().setSingleRange(range);
		},
		setSelectionBeforeElement: function (el){
			var range = rangy.createRange();

			range.selectNode(el);
			range.collapse(true);

			rangy.getSelection().setSingleRange(range);
		},
		setSelectionAfterElement: function (el){
			var range = rangy.createRange();

			range.selectNode(el);
			range.collapse(false);

			rangy.getSelection().setSingleRange(range);
		},
		setSelectionToElementStart: function (el){
			var range = rangy.createRange();

			range.selectNodeContents(el);
			range.collapse(true);

			rangy.getSelection().setSingleRange(range);
		},
		setSelectionToElementEnd: function (el){
			var range = rangy.createRange();

			range.selectNodeContents(el);
			range.collapse(false);
			if(el.childNodes && el.childNodes[el.childNodes.length - 1] && el.childNodes[el.childNodes.length - 1].nodeName === 'br'){
				range.startOffset = range.endOffset = range.startOffset - 1;
			}
			rangy.getSelection().setSingleRange(range);
		},
		setStateShiftKey: function(bS) {
			bShiftState = bS;
		},
		getStateShiftKey: function() {
			return bShiftState;
		},
		// from http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
		// topNode is the contenteditable normally, all manipulation MUST be inside this.
		insertHtml: function(html, topNode){
			var parent, secondParent, _childI, nodes, i, lastNode, _tempFrag;
			var element = angular.element("<div>" + html + "</div>");
			var range = rangy.getSelection().getRangeAt(0);
			var frag = _document.createDocumentFragment();
			var children = element[0].childNodes;
			var isInline = true;

			if(children.length > 0){
				// NOTE!! We need to do the following:
				// check for blockelements - if they exist then we have to split the current element in half (and all others up to the closest block element) and insert all children in-between.
				// If there are no block elements, or there is a mixture we need to create textNodes for the non wrapped text (we don't want them spans messing up the picture).
				nodes = [];
				for(_childI = 0; _childI < children.length; _childI++){
					var _cnode = children[_childI];
					if (_cnode.nodeName.toLowerCase() === 'p' &&
						_cnode.innerHTML.trim() === '') { // empty p element
						continue;
					}
					/****************
					 *  allow any text to be inserted...
					if((   _cnode.nodeType === 3 &&
						   _cnode.nodeValue === '\ufeff'[0] &&
						   _cnode.nodeValue.trim() === '') // empty no-space space element
						) {
						// no change to isInline
						nodes.push(_cnode);
						continue;
					}
					if(_cnode.nodeType === 3 &&
						 _cnode.nodeValue.trim() === '') { // empty text node
						continue;
					}
					*****************/
					isInline = isInline && !BLOCKELEMENTS.test(_cnode.nodeName);
					nodes.push(_cnode);
				}
				for(var _n = 0; _n < nodes.length; _n++) {
					lastNode = frag.appendChild(nodes[_n]);
				}
				if( !isInline &&
					range.collapsed &&
					/^(|<br(|\/)>)$/i.test(range.startContainer.innerHTML) ) {
					range.selectNode(range.startContainer);
				}
			}else{
				isInline = true;
				// paste text of some sort
				lastNode = frag = _document.createTextNode(html);
			}

			// Other Edge case - selected data spans multiple blocks.
			if(isInline){
				range.deleteContents();
			}else{ // not inline insert
				if(range.collapsed && range.startContainer !== topNode){
					if(range.startContainer.innerHTML && range.startContainer.innerHTML.match(/^<[^>]*>$/i)){
						// this log is to catch when innerHTML is something like `<img ...>`
						parent = range.startContainer;
						if(range.startOffset === 1){
							// before single tag
							range.setStartAfter(parent);
							range.setEndAfter(parent);
						}else{
							// after single tag
							range.setStartBefore(parent);
							range.setEndBefore(parent);
						}
					}else{
						// split element into 2 and insert block element in middle
						if(range.startContainer.nodeType === 3 && range.startContainer.parentNode !== topNode){ // if text node
							parent = range.startContainer.parentNode;
							secondParent = parent.cloneNode();
							// split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
							taDOM.splitNodes(parent.childNodes, parent, secondParent, range.startContainer, range.startOffset);

							// Escape out of the inline tags like b
							while(!VALIDELEMENTS.test(parent.nodeName)){
								angular.element(parent).after(secondParent);
								parent = parent.parentNode;
								var _lastSecondParent = secondParent;
								secondParent = parent.cloneNode();
								// split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
								taDOM.splitNodes(parent.childNodes, parent, secondParent, _lastSecondParent);
							}
						}else{
							parent = range.startContainer;
							secondParent = parent.cloneNode();
							taDOM.splitNodes(parent.childNodes, parent, secondParent, undefined, undefined, range.startOffset);
						}

						angular.element(parent).after(secondParent);
						// put cursor to end of inserted content
						//console.log('setStartAfter', parent);
						range.setStartAfter(parent);
						range.setEndAfter(parent);

						if(/^(|<br(|\/)>)$/i.test(parent.innerHTML.trim())){
							range.setStartBefore(parent);
							range.setEndBefore(parent);
							angular.element(parent).remove();
						}
						if(/^(|<br(|\/)>)$/i.test(secondParent.innerHTML.trim())) angular.element(secondParent).remove();
						if(parent.nodeName.toLowerCase() === 'li'){
							_tempFrag = _document.createDocumentFragment();
							for(i = 0; i < frag.childNodes.length; i++){
								element = angular.element('<li>');
								taDOM.transferChildNodes(frag.childNodes[i], element[0]);
								taDOM.transferNodeAttributes(frag.childNodes[i], element[0]);
								_tempFrag.appendChild(element[0]);
							}
							frag = _tempFrag;
							if(lastNode){
								lastNode = frag.childNodes[frag.childNodes.length - 1];
								lastNode = lastNode.childNodes[lastNode.childNodes.length - 1];
							}
						}
					}
				}else{
					range.deleteContents();
				}
			}

			range.insertNode(frag);
			if(lastNode){
				api.setSelectionToElementEnd(lastNode);
			}
		}

		/* NOT FUNCTIONAL YET
		 // under Firefox, we may have a selection that needs to be normalized
		 isSelectionContainerWhole_taTextElement: function (){
		 var range = rangy.getSelection().getRangeAt(0);
		 var container = range.commonAncestorContainer;
		 if (container.nodeName.toLowerCase() === 'div' &&
		 /^taTextElement/.test(container.id)) {
		 // container is the whole taTextElement
		 return true;
		 }
		 return false;
		 },
		 setNormalizedSelection: function (){
		 var range = rangy.getSelection().getRangeAt(0);
		 var container = range.commonAncestorContainer;
		 console.log(range);
		 console.log(container.childNodes);
		 if (range.collapsed) {
		 // we know what to do...
		 console.log(container.childNodes[range.startOffset]);
		 api.setSelectionToElementStart(container.childNodes[range.startOffset]);
		 }
		 },
		 */
	};
	return api;
}]).service('taDOM', function(){
	var taDOM = {
		// recursive function that returns an array of angular.elements that have the passed attribute set on them
		getByAttribute: function(element, attribute){
			var resultingElements = [];
			var childNodes = element.children();
			if(childNodes.length){
				angular.forEach(childNodes, function(child){
					resultingElements = resultingElements.concat(taDOM.getByAttribute(angular.element(child), attribute));
				});
			}
			if(element.attr(attribute) !== undefined) resultingElements.push(element);
			return resultingElements;
		},

		transferChildNodes: function(source, target){
			// clear out target
			target.innerHTML = '';
			while(source.childNodes.length > 0) target.appendChild(source.childNodes[0]);
			return target;
		},

		splitNodes: function(nodes, target1, target2, splitNode, subSplitIndex, splitIndex){
			if(!splitNode && isNaN(splitIndex)) throw new Error('taDOM.splitNodes requires a splitNode or splitIndex');
			var startNodes = document.createDocumentFragment();
			var endNodes = document.createDocumentFragment();
			var index = 0;

			while(nodes.length > 0 && (isNaN(splitIndex) || splitIndex !== index) && nodes[0] !== splitNode){
				startNodes.appendChild(nodes[0]); // this removes from the nodes array (if proper childNodes object.
				index++;
			}

			if(!isNaN(subSplitIndex) && subSplitIndex >= 0 && nodes[0]){
				startNodes.appendChild(document.createTextNode(nodes[0].nodeValue.substring(0, subSplitIndex)));
				nodes[0].nodeValue = nodes[0].nodeValue.substring(subSplitIndex);
			}
			while(nodes.length > 0) endNodes.appendChild(nodes[0]);

			taDOM.transferChildNodes(startNodes, target1);
			taDOM.transferChildNodes(endNodes, target2);
		},

		transferNodeAttributes: function(source, target){
			for(var i = 0; i < source.attributes.length; i++) target.setAttribute(source.attributes[i].name, source.attributes[i].value);
			return target;
		}
	};
	return taDOM;
});
